<!DOCTYPE html>
<html>
<head>
<title>shiro</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
/* Author: Nicolas Hery - http://nicolashery.com */
/* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */
/* Source: https://github.com/nicolahery/markdownpad-github */

/* RESET
=============================================================================*/

html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
}

/* BODY
=============================================================================*/

body {
  font-family: Helvetica, arial, freesans, clean, sans-serif;
  font-size: 14px;
  line-height: 1.6;
  color: #333;
  background-color: #fff;
  padding: 20px;
  max-width: 960px;
  margin: 0 auto;
}

body>*:first-child {
  margin-top: 0 !important;
}

body>*:last-child {
  margin-bottom: 0 !important;
}

/* BLOCKS
=============================================================================*/

p, blockquote, ul, ol, dl, table, pre {
  margin: 15px 0;
}

/* HEADERS
=============================================================================*/

h1, h2, h3, h4, h5, h6 {
  margin: 20px 0 10px;
  padding: 0;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}

h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
  font-size: inherit;
}

h1 {
  font-size: 28px;
  color: #000;
}

h2 {
  font-size: 24px;
  border-bottom: 1px solid #ccc;
  color: #000;
}

h3 {
  font-size: 18px;
}

h4 {
  font-size: 16px;
}

h5 {
  font-size: 14px;
}

h6 {
  color: #777;
  font-size: 14px;
}

body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
  margin-top: 0;
  padding-top: 0;
}

a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
  margin-top: 0;
  padding-top: 0;
}

h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
  margin-top: 10px;
}

/* LINKS
=============================================================================*/

a {
  color: #4183C4;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* LISTS
=============================================================================*/

ul, ol {
  padding-left: 30px;
}

ul li > :first-child, 
ol li > :first-child, 
ul li ul:first-of-type, 
ol li ol:first-of-type, 
ul li ol:first-of-type, 
ol li ul:first-of-type {
  margin-top: 0px;
}

ul ul, ul ol, ol ol, ol ul {
  margin-bottom: 0;
}

dl {
  padding: 0;
}

dl dt {
  font-size: 14px;
  font-weight: bold;
  font-style: italic;
  padding: 0;
  margin: 15px 0 5px;
}

dl dt:first-child {
  padding: 0;
}

dl dt>:first-child {
  margin-top: 0px;
}

dl dt>:last-child {
  margin-bottom: 0px;
}

dl dd {
  margin: 0 0 15px;
  padding: 0 15px;
}

dl dd>:first-child {
  margin-top: 0px;
}

dl dd>:last-child {
  margin-bottom: 0px;
}

/* CODE
=============================================================================*/

pre, code, tt {
  font-size: 12px;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
}

code, tt {
  margin: 0 0px;
  padding: 0px 0px;
  white-space: nowrap;
  border: 1px solid #eaeaea;
  background-color: #f8f8f8;
  border-radius: 3px;
}

pre>code {
  margin: 0;
  padding: 0;
  white-space: pre;
  border: none;
  background: transparent;
}

pre {
  background-color: #f8f8f8;
  border: 1px solid #ccc;
  font-size: 13px;
  line-height: 19px;
  overflow: auto;
  padding: 6px 10px;
  border-radius: 3px;
}

pre code, pre tt {
  background-color: transparent;
  border: none;
}

kbd {
    -moz-border-bottom-colors: none;
    -moz-border-left-colors: none;
    -moz-border-right-colors: none;
    -moz-border-top-colors: none;
    background-color: #DDDDDD;
    background-image: linear-gradient(#F1F1F1, #DDDDDD);
    background-repeat: repeat-x;
    border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
    border-image: none;
    border-radius: 2px 2px 2px 2px;
    border-style: solid;
    border-width: 1px;
    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
    line-height: 10px;
    padding: 1px 4px;
}

/* QUOTES
=============================================================================*/

blockquote {
  border-left: 4px solid #DDD;
  padding: 0 15px;
  color: #777;
}

blockquote>:first-child {
  margin-top: 0px;
}

blockquote>:last-child {
  margin-bottom: 0px;
}

/* HORIZONTAL RULES
=============================================================================*/

hr {
  clear: both;
  margin: 15px 0;
  height: 0px;
  overflow: hidden;
  border: none;
  background: transparent;
  border-bottom: 4px solid #ddd;
  padding: 0;
}

/* TABLES
=============================================================================*/

table th {
  font-weight: bold;
}

table th, table td {
  border: 1px solid #ccc;
  padding: 6px 13px;
}

table tr {
  border-top: 1px solid #ccc;
  background-color: #fff;
}

table tr:nth-child(2n) {
  background-color: #f8f8f8;
}

/* IMAGES
=============================================================================*/

img {
  max-width: 100%
}
</style>
</head>
<body>
<h1>登录认证</h1>
<h3>1.创建数据库</h3>
<pre><code>
/*
SQLyog Enterprise - MySQL GUI v8.14
MySQL - 5.5.20 : Database - test
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` VARCHAR(255) NOT NULL,
  `name` VARCHAR(255) NOT NULL COMMENT '姓名',
  `username` VARCHAR(255) NOT NULL COMMENT '用户名',
  `password` VARCHAR(255) NOT NULL DEFAULT '12345' COMMENT '密码',
  `phone` VARCHAR(11) NOT NULL COMMENT '手机号',
  `gender` VARCHAR(4) NOT NULL COMMENT '性别',
  `identity` VARCHAR(4) NOT NULL COMMENT '身份',
  `is_enable` VARCHAR(4) NOT NULL COMMENT '启用状态',
  `last_login_time` DATETIME DEFAULT NULL COMMENT '上一次登录时间',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

/*Data for the table `user` */

INSERT  INTO `user`(`id`,`name`,`username`,`password`,`phone`,`gender`,`identity`,`is_enable`,`last_login_time`) VALUES
 ('2023','系统管理员','2','2','18888888888',2,2,3,'2022-11-25 00:15:42'),
 ('mhxy1218','沐雨橙风ιε','1','1','16666666666',1,1,0,'2023-07-02 00:00:29');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

</code></pre>
<h3>2.创建数据表对应的实体类</h3>
<pre><code>
package com.abc.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 用户数据的实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User extends BaseEntity {

	private static final long serialVersionUID = -1197587481888673428L;

	private String id;
	/**
	 * 姓名
	 */
	private String name;
	/**
	 * 用户名
	 */
	private String username;
	/**
	 * 密码
	 */
	private String password;
	/**
	 * 手机
	 */
	private String phone;
	/**
	 * 性别
	 */
	private String gender;
	/**
	 * 是否启用
	 */
	private String isEnable;
	/**
	 * 最后登录时间
	 */
	private String lastLoginTime;

}

</code></pre>
<h3>3.在springboot的启动类或者任意一个配置类上使用注解@MapperScan注解配置mybatis的mapper包扫描路径</h3>
<pre><code>
@MapperScan("com.abc.mapper")
</code></pre>
<h3>4.创建一个配置类</h3>
<pre><code>
创建 com.abc.config.MybatisConfig 配置类
并添加如下注解
@Configuration
@MapperScan("com.abc.mapper")
</code></pre>
<pre><code>
package com.abc.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis配置类
 */
@Configuration
@MapperScan("com.abc.mapper")
public class MybatisConfig {
}

</code></pre>
<h3>5.创建一个配置类</h3>
<pre><code>
创建 com.abc.realm.UserRealm
并继承 AuthorizingRealm ，实现AuthorizingRealm的两个认证和授权的抽象方法。
</code></pre>
<pre><code>
package com.abc.realm;

import com.abc.entity.User;
import com.abc.mapper.UserMapper;
import com.abc.service.ex.GlobalException;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserMapper userMapper;
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        // 得到用户名
        String username = token.getUsername();
        // 根据用户名查询用户信息
        User user = userMapper.selectByUsername(username);

        if (user == null) {
            throw new GlobalException( "登录失败，用户不存在~");
        }
        if ("0".equals(user.getIsEnable())) {
            String password = new String(token.getPassword());

            if (user.getPassword().equals(password)) {
                return new SimpleAuthenticationInfo(user, password, username);
            } else {
                throw new GlobalException("用户名禁止使用！");
            }
        }

        return null;
    }
}
</code></pre>
<h3>6.创建Shiro配置类</h3>
<pre><code>
创建 com.abc.config.ShiroConfig

添加注解 @Configuration

配置安全管理器
配置Shiro过滤器工厂
 添加资源访问规则，配置登录页面和登录接口的地址可以不需要登录认证就能访问，同时/html/home.html需要登录才能访问。
</code></pre>
<pre><code>
package com.abc.config;

import com.abc.realm.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    /**
     * 配置安全管理器
     * @param userRealm UserRealm
     * @return DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(userRealm);

        return securityManager;
    }

    /**
     * 配置Shiro过滤器工厂
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 注册安全管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        /*
         * 设置登录页面的地址
         * 当用户访问认证资源的时候，如果用户没有登录，那么就会跳转到指定的页面
         */
        shiroFilterFactoryBean.setLoginUrl("/login.html");

        // 定义资源访问规则
        Map&lt;String, String&gt; map = new LinkedHashMap<>();

        /*
         * 过滤器说明
         * anon：不需要认证就可以访问的资源
         * authc：需要登录认证才能访问的资源
         */
        map.put("/home.html", "authc");

        // 不需要认证就能访问
        map.put("/login.html", "anon");
        map.put("/users/login", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
}

</code></pre>
<h3>7.实现登录功能 控制器层</h3>
<pre><code>
在UserController类中添加一个login()方法，新建UserLoginDTO对象来接收前端传来的用户名和密码，同时通过validation验证这两个字段。
</code></pre>
<pre><code>
@RequestMapping(path = "/login", method = RequestMethod.POST)
    public JsonResult&lt;Void&gt; login(UserLoginDTO loginDTO) {
        userService.login(loginDTO);

        return new JsonResult(1);
    }
</code></pre>
<h3>8.实现登录功能 业务层</h3>
<pre><code>
UserServiceImpl中实现用户登录的业务代码，当我们调用Subject的login()方法时，
会执行UserRealm下面的认证方法 doGetAuthenticationInfo()
实现认证的方法，在shiro框架内部会去调用realm的认证方法。
</code></pre>
<pre><code>
    @Override
    public void login(UserLoginDTO loginDTO) {
        String username = loginDTO.getUsername();
        // 根据用户名查询用户信息
        User user = userMapper.selectByUsername(username);
        System.out.println(user);
        if (user != null) {
            if ("0".equals(user.getIsEnable())) {
                // shiro登录认证
                UsernamePasswordToken token = new UsernamePasswordToken(username, loginDTO.getPassword());
                System.out.println(token);
                Subject subject = SecurityUtils.getSubject();
                try {
                    subject.login(token);
                }catch(AuthenticationException e){
                    throw new GlobalException("用户名或密码错误~");
                }
                // 设置session失效时间：永不超时
                subject.getSession().setTimeout(-1001);
            } else {
                throw new GlobalException("账号已被锁定，禁止登录！");
            }
        } else {
            throw new GlobalException("用户名不存在~");
        }
    }
</code></pre>
<h3>9.当没有登录时访问需要登录认证才能访问的资源时。url地址栏带jsessionid的解决方式</h3>
<pre><code>
新建 com.abc.shiro.MyShiroHttpServletResponse 继承 ShiroHttpServletResponse
  重写其方法 toEncoded
</code></pre>
<pre><code>
package com.abc.shiro;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.servlet.ShiroHttpServletResponse;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;


public class MyShiroHttpServletResponse extends ShiroHttpServletResponse {
    public MyShiroHttpServletResponse(HttpServletResponse wrapped, ServletContext context, ShiroHttpServletRequest request) {
        super(wrapped, context, request);
    }
    @Override
    protected String toEncoded(String url, String sessionId) {
        if ((url == null) || (sessionId == null))
            return (url);
        String path = url;
        String query = "";
        String anchor = "";
        int question = url.indexOf('?');
        if (question >= 0) {
            path = url.substring(0, question);
            query = url.substring(question);
        }
        int pound = path.indexOf('#');
        if (pound >= 0) {
            anchor = path.substring(pound);
            path = path.substring(0, pound);
        }
        StringBuilder sb = new StringBuilder(path);
        //重写toEncoded方法，注释掉这几行代码就不会再生成JESSIONID了。
//        if (sb.length() > 0) { // session id param can't be first.
//            sb.append(";");
//            sb.append(DEFAULT_SESSION_ID_PARAMETER_NAME);
//            sb.append("=");
//            sb.append(sessionId);
//        }
        sb.append(anchor);
        sb.append(query);
        return (sb.toString());
    }
}
</code></pre>
<hr>

<h1>授权功能</h1>
<h3>1.实现授权的代码</h3>
<pre><code>
在 com.abc.realm.UserRealm 类下的 doGetAuthorizationInfo 方法里里面实现授权的代码
</code></pre>
<pre><code>
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) principalCollection.getPrimaryPrincipal();
        System.out.println(user);
        //根据user查询拥有的权限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set&lt;String&gt; permissions = new HashSet<>();

        permissions.add("/users/delete");
        permissions.add("/users/update");

        authorizationInfo.setStringPermissions(permissions);

        return authorizationInfo;
    }
</code></pre>
<h3>2. controller 模拟请求删除和修改</h3>
<pre><code>
@RequestMapping(path = "/delete", method = RequestMethod.POST)
    public JsonResult&lt;Void&gt; delete() {
        return new JsonResult(1,"删除成功");
    }

    @RequestMapping(path = "/update", method = RequestMethod.POST)
    public JsonResult&lt;Void&gt; update() {
        return new JsonResult(1,"修改成功");
    }
</code></pre>
<h3>3.自定义过滤器实现鉴权</h3>
<pre><code>
自定义过滤器 com.abc.filter.AuthorizationFilter 实现鉴权功能。
</code></pre>
<pre><code>
package com.abc.filter;

import com.abc.util.JsonResult;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 鉴权过滤器
 */
@WebFilter
public class AuthorizationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        String requestURI = request.getRequestURI();
        Subject subject = SecurityUtils.getSubject();

        if (subject != null && !subject.isPermitted(requestURI)) {
            HttpServletResponse response = (HttpServletResponse) resp;
            response.setContentType("application/json;charset=utf-8");

            // 构建返回对象
            String data = "正在访问未授权的资源";

            response.getWriter().write(data);
            return;
        }

        chain.doFilter(req, resp);
    }

}

</code></pre>
<h3>4.把自定义过滤器加入到shiro的过滤器链 ShiroConfig 中</h3>
<pre><code>
        // 设置自定义过滤器
        map.put("/users/delete", "authorization");
        map.put("/users/update", "authorization");
        Map&lt;String, Filter&gt; filters = shiroFilterFactoryBean.getFilters();
        filters.put("authorization", new AuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filters);
</code></pre>
</body>
</html>
<!-- This document was created with MarkdownPad, the Markdown editor for Windows (http://markdownpad.com) -->
